﻿Imports Microsoft.WindowsCE.Forms
Imports System.Runtime.InteropServices
Imports System.Text
Imports Tags
Imports Tags.ID3
Imports Tags.ID3.ID3v1
Imports Tags.ID3.ID3v2
Imports Tags.ID3.ID3v2Frames
Imports AvianWaves.AvianPlay.Language

Namespace Library
  Public Class Monitor
    Inherits MessageWindow

    Private _rescanThread As Threading.Thread = Nothing
    Private _lastScanPaths As New List(Of String)
    Private _lastExcludePaths As New List(Of String)
    Private _killRescanThread As Boolean = False
    Private _rescanThreadStopwatch As New Stopwatch
    Private WithEvents _threadCheckTimer As New Timer
    Private WithEvents _realtimeMonitor As New MonitorRealtime

    Private Shared _thumbHeight As Integer = 0
    Private Shared _dpiFactor As Single = 0.0F

    Private _scanAgain As Boolean = False
    Private _advNotifString As String = ""

    Public Enum UpdateAction
      NoAction
      Insert
      Update
    End Enum

    Public Event MonitorUpdateStatusChanged(ByVal monitorIsUpdating As Boolean, ByVal realtimeIsUpdating As Boolean)
    Public Event RealtimeUpdateManagedPlaylists(ByVal oldFileName As String, ByVal newFileName As String)
    Public Event WriteErrorOnMainThread(ByVal err As String)
    Public Event StartFullScanAfterDelayOnMainThread()

    Private _lastMonitorStatus As Boolean = False
    Private _lastRealtimeMonitorStauts As Boolean = False


    Public Sub New(ByVal newdpifactor As Single)
      _dpiFactor = newdpifactor
    End Sub

    Public ReadOnly Property AdvancedNotificationString() As String
      Get
        Return Me._advNotifString
      End Get
    End Property

    Public Function RescanMonitoredPaths() As Boolean
      If Me._rescanThread Is Nothing Then
        Me._rescanThread = New Threading.Thread(AddressOf RescanMonitoredPathsThread)
        Me._rescanThread.Name = "RescanThread"
        Me._rescanThread.Priority = Threading.ThreadPriority.BelowNormal
        Me._rescanThread.IsBackground = True
        Me._killRescanThread = False

        Me._lastScanPaths.Clear()
        Me._lastScanPaths.AddRange(Settings.LibraryScanPaths)
        Me._lastExcludePaths.Clear()
        Me._lastExcludePaths.AddRange(Settings.LibraryExcludePaths)

        SyncLock MainThreadPool
          MainThreadPool.Add(Me._rescanThread)
        End SyncLock

        Me._rescanThread.Start()
        If Settings.ShowDetailedLibraryMonitorMessages Then Me._advNotifString = GetLanguageString("LibraryLoading")
        Me.RaiseMonitorUpdateStatusChanged(True)
        Me._threadCheckTimer.Interval = 250
        Me._threadCheckTimer.Enabled = True
        Return True
      Else
        Return False
      End If
    End Function

    Public Sub StartRealtimeMonitor()
      Me._realtimeMonitor.StartRealtimeMonitor(Settings.LibraryScanPaths, Settings.LibraryExcludePaths)
    End Sub

    Public Sub StopRealtimeMonitor()
      Me._realtimeMonitor.EndRealtimeMonitor()
    End Sub

    Public Sub UpdateSingleFile(ByVal filePath As String)
      Me._realtimeMonitor.UpdateFileNow(filePath)
    End Sub

    Public ReadOnly Property IsMonitorFullScanActive() As Boolean
      Get
        Return (Me._rescanThread IsNot Nothing)
      End Get
    End Property

    Public ReadOnly Property IsMonitorOrRelatimeScanActive() As Boolean
      Get
        Return Me._lastMonitorStatus Or Me._lastRealtimeMonitorStauts
      End Get
    End Property

    Public ReadOnly Property IsRealtimeActive() As Boolean
      Get
        Return Me._realtimeMonitor.isMonitorActive
      End Get
    End Property

    Public ReadOnly Property IsAnyScanThreadActive() As Boolean
      Get
        Return Me.IsMonitorFullScanActive OrElse Me.IsRealtimeActive
      End Get
    End Property

    Public Sub StopAllMonitors()
      Me.StopFullScanThread()
      Me._realtimeMonitor.EndRealtimeMonitor()
    End Sub

    ''' <summary>
    ''' This is an asynchronous call!  It does not immediately terminate (abort) threads.  It requests that the thread finish processing
    ''' as quickly as possible.  The monitor timer will notify via the MonitorUpdateStatusChanged event at a resolution of once thread
    ''' check per second.  If you need more responsiveness, read the IsMonsterScanning property.  Just FYI, aborting the thread results
    ''' in some of unexpected exceptions since apparently the SQL CE database API doesn't like to be interrupted in the middle of an
    ''' operation.
    ''' </summary>
    Public Sub StopFullScanThread()
      If Me._rescanThread IsNot Nothing Then
        Me._killRescanThread = True
      End If
    End Sub

    Private Sub RescanMonitoredPathsThread()
      Try
        ' Timer to track how long the scan is taking
        Me._rescanThreadStopwatch.Reset()
        Me._rescanThreadStopwatch.Start()

        ' Do we have valid height for the thumbnails?
        If _thumbHeight <= 0 AndAlso _dpiFactor > 0.0F Then
          Dim aple As New AvianPlay.Playlists.AVPLPlaylistEntry
          _thumbHeight = aple.DrawPrettyListItemHeight(Graphics.FromImage(New Bitmap(50, 200)), _dpiFactor)
        End If

        ' Initalize the database connection
        Using db As New AvianPlayDB
          ' Scan the file system for the above filespecs
          For Each s As String In Me._lastScanPaths
            RescanRecursive(s, db)
          Next

          ' Look at every file already in the database to see if it still exists in the file system or if it's been excluded from the scanned paths.  Set the flag accordingly.
          If Settings.ShowDetailedLibraryMonitorMessages Then Me._advNotifString = GetLanguageString("AdvCheckFileExists")
          db.UpdateAllFileNotFoundStatus()

          ' Finally, for all "not found" items, delete the ones older than 14 days old
          If Settings.ShowDetailedLibraryMonitorMessages Then Me._advNotifString = GetLanguageString("AdvRemovingExpiredFiles")
          db.DeleteFileDBNotFoundOlderThan(Date.Now.AddDays(-14))

          ' Check if we scan again
          If Me._scanAgain AndAlso Me._realtimeMonitor.IsMonitorActive Then
            RaiseEvent StartFullScanAfterDelayOnMainThread()
          End If
        End Using
      Catch ex As Threading.ThreadAbortException
        ' Always safe to ignore this -- the thread is being terminated
      Catch ex As Exception When Me._killRescanThread = True
        ' Eat all exceptions when we are killing the thread (SQL CE throws random exceptions sometimes - the bastard)
      Finally
        SyncLock MainThreadPool
          MainThreadPool.Remove(Me._rescanThread)
        End SyncLock

        ' Set the thead reference to null to notify that we are done processing
        Me._rescanThread = Nothing
        Me._rescanThreadStopwatch.Stop()
        Me._scanAgain = False
      End Try
    End Sub

    Private Sub RescanRecursive(ByVal path As String, ByVal db As AvianPlayDB)
      If Me._killRescanThread OrElse Not System.IO.Directory.Exists(path) OrElse IsExcludedPath(path, Me._lastExcludePaths) Then
        Exit Sub
      End If

      If Settings.ShowDetailedLibraryMonitorMessages Then
        Me._advNotifString = GetLanguageString("AdvScanning").Replace("%", path)
      End If

      ' SCAN AUDIO FILES
      Try
        For Each em As Settings.AudioExtensionMap In Settings.GetAudioExtensionMaps
          ' Do we need to pause for a moment because the scan is taking too long? (every minute, pause 1 second to be a good citizen and allow other threads to run)
          If Me._rescanThreadStopwatch.ElapsedMilliseconds > 60000 Then
            Threading.Thread.Sleep(1000)
            Me._rescanThreadStopwatch.Reset()
            Me._rescanThreadStopwatch.Start()
          End If
          For Each file As String In System.IO.Directory.GetFiles(path, em.FileSpec)
            ' Do we need to pause for a moment because the scan is taking too long? (every minute, pause 1 second to be a good citizen and allow other threads to run)
            If Me._rescanThreadStopwatch.ElapsedMilliseconds > 60000 Then
              Threading.Thread.Sleep(1000)
              Me._rescanThreadStopwatch.Reset()
              Me._rescanThreadStopwatch.Start()
            End If

            If Me._killRescanThread Then Exit Sub
            Dim err As String = UpdateAudioFileInDatabase(file, em, db)
            If err IsNot Nothing AndAlso err.Length > 0 Then
              RaiseEvent WriteErrorOnMainThread(err)
            End If

            ' Do a mandatory short pause to let other lower priority threads to get some love (it seems to help "unlock" activesync)
            Threading.Thread.Sleep(1)
          Next
          ' Do a mandatory short pause to let other lower priority threads to get some love (it seems to help "unlock" activesync)
          Threading.Thread.Sleep(1)
        Next
      Catch ex As Threading.ThreadAbortException
        Throw
      Catch ex As Exception
        ErrorLog.WriteToErrorLog("RescanRecursive : Unable to enumerate audio files in path " & path & " :: " & ex.ToString)
      End Try



      ' SCAN PLAYLIST FILES
      Try
        For Each em As Settings.PlaylistExtensionMap In Settings.GetPlaylistExtensionMaps
          ' Do we need to pause for a moment because the scan is taking too long? (every minute, pause 1 second to be a good citizen and allow other threads to run)
          If Me._rescanThreadStopwatch.ElapsedMilliseconds > 60000 Then
            Threading.Thread.Sleep(1000)
            Me._rescanThreadStopwatch.Reset()
            Me._rescanThreadStopwatch.Start()
          End If
          For Each file As String In System.IO.Directory.GetFiles(path, em.FileSpec)
            ' Do we need to pause for a moment because the scan is taking too long? (every minute, pause 1 second to be a good citizen and allow other threads to run)
            If Me._rescanThreadStopwatch.ElapsedMilliseconds > 60000 Then
              Threading.Thread.Sleep(1000)
              Me._rescanThreadStopwatch.Reset()
              Me._rescanThreadStopwatch.Start()
            End If

            If Me._killRescanThread Then Exit Sub
            Dim err As String = UpdatePlaylistFileInDatabase(file, em, db)
            If err IsNot Nothing AndAlso err.Length > 0 Then
              RaiseEvent WriteErrorOnMainThread(err)
            End If

            ' Do a mandatory short pause to let other lower priority threads to get some love (it seems to help "unlock" activesync)
            Threading.Thread.Sleep(1)
          Next
          ' Do a mandatory short pause to let other lower priority threads to get some love (it seems to help "unlock" activesync)
          Threading.Thread.Sleep(1)
        Next
      Catch ex As Threading.ThreadAbortException
        Throw
      Catch ex As Exception
        ErrorLog.WriteToErrorLog("RescanRecursive : Unable to enumerate playlist files in path " & path & " :: " & ex.ToString)
      End Try

      Try
        If path IsNot Nothing AndAlso path.Trim.Length > 0 Then
          Dim paths() As String = System.IO.Directory.GetDirectories(path)
          If paths IsNot Nothing AndAlso paths.Count > 0 Then
            For Each subpath As String In paths
              If Me._killRescanThread Then Exit Sub
              Me.RescanRecursive(subpath, db)
            Next
          End If
        End If
      Catch ex As Threading.ThreadAbortException
        Throw
      Catch ex As Exception
        ErrorLog.WriteToErrorLog("RescanRecursive : Unable to enumerate folders in path " & path & " :: " & ex.ToString)
      End Try
    End Sub

    Public Shared Function IsIncludedPath(ByVal path As String, ByVal ipaths As List(Of String)) As Boolean
      For Each ip As String In ipaths
        ' Look for exact path
        If ip.Length = path.Length AndAlso String.Compare(ip, path, True) = 0 Then
          Return True
        End If

        ' Look for subpath
        If Not ip.EndsWith("\") Then
          ip &= "\"
          If path.Length > ip.Length AndAlso String.Compare(ip, Left(path, ip.Length), True) = 0 Then
            Return True
          End If
        End If
      Next

      Return False
    End Function

    Public Shared Function IsExcludedPath(ByVal path As String, ByVal epaths As List(Of String)) As Boolean
      For Each ep As String In epaths
        ' Look for exact path
        If ep.Length = path.Length AndAlso String.Compare(ep, path, True) = 0 Then
          Return True
        End If

        ' Look for subpath
        If Not ep.EndsWith("\") Then
          ep &= "\"
          If path.Length > ep.Length AndAlso String.Compare(ep, Left(path, ep.Length), True) = 0 Then
            Return True
          End If
        End If
      Next

      Return False
    End Function

    Public Shared Function IsFileInScanPathAndExists(ByVal path As String, ByVal ipaths As List(Of String), ByVal epaths As List(Of String)) As Boolean
      Dim tpath As String = path.Trim
      If IsIncludedPath(tpath, ipaths) AndAlso Not IsExcludedPath(tpath, epaths) Then
        Return IO.File.Exists(tpath)
      End If
      Return False
    End Function

    Private Shared Function CanReadFile(ByVal file As String) As Boolean
      Dim fileInputCheck As IO.FileStream = Nothing
      Try
        fileInputCheck = New IO.FileStream(file, IO.FileMode.Open, IO.FileAccess.Read, IO.FileShare.Read)
        fileInputCheck.ReadByte()
        If fileInputCheck.CanRead Then
          Return True
        End If
      Catch
      Finally
        If fileInputCheck IsNot Nothing Then
          Try : fileInputCheck.Close() : Catch : End Try
        End If
      End Try

      Return False
    End Function

    Friend Shared Function UpdateAudioFileInDatabase(ByVal file As String, ByVal em As Settings.AudioExtensionMap, ByVal db As AvianPlayDB) As String
      UpdateAudioFileInDatabase = ""
      Try
        ' Make sure the file both exists and the size is larger than 0
        If IO.File.Exists(file) AndAlso New IO.FileInfo(file).Length > 0 Then
          Dim readFailed As Boolean = Not CanReadFile(file) ' Check to see if we can read the file
          Dim getTagFailed As Boolean = True

          Dim ua As UpdateAction = UpdateAction.NoAction
          Dim fi As New System.IO.FileInfo(file) ' Get file information
          Dim ft As AvianPlayDB.AudioFileTable = Nothing

          ' Do we have any database entry for this file (found in the last scan or not)?
          Dim fDate As Date = db.GetAudioFileLastModifyDate(file, True)

          If fDate > Date.MinValue Then
            ' We have an entry for this file, check the update times
            If fDate <> fi.LastWriteTime Then
              ua = UpdateAction.Update    ' Modify times are different, so we update the entry
            Else
              ua = UpdateAction.NoAction  ' Modify times are identical, no action is needed, just update the last scan time
            End If
          Else
            ua = UpdateAction.Insert      ' Insert the new item into the database
          End If

          If ua <> UpdateAction.NoAction Then
            ' Reset the file table structure
            ft = New AvianPlayDB.AudioFileTable

            Select Case em.Type
              Case Settings.AudioFileType.MP3 ' Parse MP3 here! -----------------------------------------------------------------------------------------
                Try
                  ft.Format = "MP3"

                  ' Fetch the MP3 ID Tags
                  Dim id3 As New Tags.ID3.ID3Info(file, True)

                  ' Look for ID3 V1 tags (if we also have ID3 V2, we will overwrite it in the next step)
                  If id3.ID3v1Info.HaveTag Then
                    ' Fetch text data
                    ft.Artist = id3.ID3v1Info.Artist
                    ft.Title = id3.ID3v1Info.Title
                    ft.Album = id3.ID3v1Info.Album
                    ft.Comments = id3.ID3v1Info.Comment
                    ft.TrackNumber = CInt(id3.ID3v1Info.TrackNumber)
                    ft.Genre = db.GetID3V1GenreByIndex(id3.ID3v1Info.Genre)
                    Try
                      ft.Year = Integer.Parse(id3.ID3v1Info.Year)
                    Catch ex As Threading.ThreadAbortException
                      Throw
                    Catch
                      ft.Year = 0
                    End Try
                  End If

                  ' Look for ID3 V2 tags and overwrite any V1 tags, if present
                  If id3.ID3v2Info.HaveTag Then
                    ' Fetch text frames
                    For Each frm As TextFrames.TextFrame In id3.ID3v2Info.TextFrames
                      Select Case frm.FrameID.ToUpper
                        Case "TPE1", "TP1" ' Artist
                          ft.Artist = frm.Text
                        Case "TIT2", "TT2" ' Title
                          ft.Title = frm.Text
                        Case "TCOP", "TCR" ' Copyright
                          ft.Copyright = frm.Text
                        Case "TCON", "TCO" ' Genre
                          ft.Genre = frm.Text
                        Case "TPE2", "TP2" ' Album Artist
                          ft.AlbumArtist = frm.Text
                        Case "TALB", "TAL" ' Album
                          ft.Album = frm.Text
                        Case "TRCK", "TRK" ' Track number
                          ft.TrackNumber = ParseID3V2TrackString(frm.Text)
                        Case "TYER", "TYE" ' Year
                          Try
                            ft.Year = Integer.Parse(frm.Text)
                          Catch ex As Threading.ThreadAbortException
                            Throw
                          Catch
                            ft.Year = 0
                          End Try
                        Case "COMM", "COM" ' Comments
                          ft.Comments = frm.Text
                      End Select
                    Next

                    ' Rating frames
                    For Each frm As TextFrames.PopularimeterFrame In id3.ID3v2Info.PopularimeterFrames
                      Select Case frm.FrameID.ToUpper
                        Case "POPM"
                          ft.Rating = frm.Rating
                      End Select
                      Exit For ' We only support one POPM tag at this time, so we use the first one found and exit the for loop
                    Next

                    ' Fetch picture frame
                    Dim aabmp As Bitmap = Nothing
                    For Each frm As BinaryFrames.AttachedPictureFrame In id3.ID3v2Info.AttachedPictureFrames
                      If frm.PictureType = BinaryFrames.AttachedPictureFrame.PictureTypes.Cover_Front Then
                        aabmp = frm.Picture
                        Exit For
                      End If
                    Next

                    If aabmp IsNot Nothing Then
                      ' Convert thumbnails to bytes
                      ft.AlbumArt = CreateAblumArtThumbnail(aabmp, 480)
                      ft.AlbumArtThumbnail = CreateAblumArtThumbnail(aabmp, _thumbHeight)

                      ' Free the temporary bitmap
                      aabmp.Dispose()
                    End If
                  End If

                  getTagFailed = False
                Catch ex As Exception
                  UpdateAudioFileInDatabase &= "UpdateAudioFileInDatabase: " & file & " - MP3 Tag Parsing Failed." & vbCrLf & ex.ToString
                End Try


              Case Settings.AudioFileType.OGG ' Parse OGG here! -----------------------------------------------------------------------------------------
                Try
                  ft.Format = "OGG"

                  ' Fetch the ogg tags
                  Dim xiph As New Luminescence.Xiph.OggTagger(file)
                  ft.Artist = StringNoNull(xiph.Artist)
                  ft.Title = StringNoNull(xiph.Title)
                  ft.Copyright = StringNoNull(xiph.Copyright)
                  ft.Genre = StringNoNull(xiph.Genre)
                  ft.AlbumArtist = StringNoNull(xiph.AlbumArtist)
                  ft.Album = StringNoNull(xiph.Album)
                  ft.TrackNumber = ParseID3V2TrackString(StringNoNull(xiph.TrackNumber))
                  ft.Year = FindYearInDateString(StringNoNull(xiph.Date))
                  ft.Comments = StringNoNull(xiph.Comments)
                  ft.Rating = xiph.Rating

                  For Each id3pf As Luminescence.Xiph.ID3PictureFrame In xiph.FlacArts
                    If id3pf.PictureType = Luminescence.Xiph.ID3PictureType.FrontCover Then
                      If id3pf.Picture IsNot Nothing Then
                        ft.AlbumArt = CreateAblumArtThumbnail(id3pf.Picture, 480)
                        ft.AlbumArtThumbnail = CreateAblumArtThumbnail(id3pf.Picture, _thumbHeight)
                        Exit For
                      End If
                    End If
                  Next

                  getTagFailed = False
                Catch ex As Exception
                  UpdateAudioFileInDatabase &= "UpdateAudioFileInDatabase: " & file & " - OGG/Xiph Tag Parsing Failed." & vbCrLf & ex.ToString
                End Try


              Case AudioFileType.FLAC         ' Parse FLAC here! ----------------------------------------------------------------------------------------
                Try
                  ft.Format = "FLAC"

                  ' Fetch the ogg tags
                  Dim xiph As New Luminescence.Xiph.FlacTagger(file)
                  ft.Artist = StringNoNull(xiph.Artist)
                  ft.Title = StringNoNull(xiph.Title)
                  ft.Copyright = StringNoNull(xiph.Copyright)
                  ft.Genre = StringNoNull(xiph.Genre)
                  ft.AlbumArtist = StringNoNull(xiph.AlbumArtist)
                  ft.Album = StringNoNull(xiph.Album)
                  ft.TrackNumber = ParseID3V2TrackString(StringNoNull(xiph.TrackNumber))
                  ft.Year = FindYearInDateString(StringNoNull(xiph.Date))
                  ft.Comments = StringNoNull(xiph.Comments)
                  ft.Rating = xiph.Rating

                  For Each id3pf As Luminescence.Xiph.ID3PictureFrame In xiph.Arts
                    If id3pf.PictureType = Luminescence.Xiph.ID3PictureType.FrontCover Then
                      If id3pf.Picture IsNot Nothing Then
                        ft.AlbumArt = CreateAblumArtThumbnail(id3pf.Picture, 480)
                        ft.AlbumArtThumbnail = CreateAblumArtThumbnail(id3pf.Picture, _thumbHeight)
                        Exit For
                      End If
                    End If
                  Next

                  getTagFailed = False
                Catch ex As Exception
                  UpdateAudioFileInDatabase &= "UpdateAudioFileInDatabase: " & file & " - FLAC/Xiph Tag Parsing Failed." & vbCrLf & ex.ToString
                End Try


              Case AudioFileType.WMA          ' Parse WMA here! -----------------------------------------------------------------------------------------
                Try
                  ft.Format = "WMA"

                  ' Fetch the WMA ID Tags
                  Dim asf As New Tags.ASF.ASFTagInfo(file, True)

                  ' Check the extended tags first
                  With asf.ExContentDescription.Descriptors
                    For Each k As String In .Keys
                      Select Case k.Trim.ToUpper
                        Case "WM/ALBUM", "WM/ALBUMTITLE"
                          ft.Album = StringNoNull(.Item(k).Value.ToString)
                        Case "WM/GENRE"
                          ft.Genre = StringNoNull(.Item(k).Value.ToString)
                        Case "WM/TRACK", "WM/TRACKNUMBER"
                          ft.TrackNumber = ParseID3V2TrackString(StringNoNull(.Item(k).Value.ToString))
                        Case "WM/YEAR"
                          ft.Year = FindYearInDateString(StringNoNull(.Item(k).Value.ToString))
                        Case "WM/ALBUMARTIST", "WM/BAND"
                          ft.AlbumArtist = StringNoNull(.Item(k).Value.ToString)
                        Case "WM/ARTIST"
                          ft.Artist = StringNoNull(.Item(k).Value.ToString)
                        Case "WM/SHAREDUSERRATING"
                          ' Try a couple different formats
                          Dim str As String = StringNoNull(.Item(k).Value.ToString)
                          Try
                            Dim i As Integer = Integer.Parse(str)
                            If i > Byte.MaxValue Then
                              i = Byte.MaxValue
                            ElseIf i < Byte.MinValue Then
                              i = Byte.MinValue
                            End If
                            ft.Rating = CByte(i)
                          Catch
                            ' Rating is invalid, so we just ignore it
                          End Try

                        Case "WM/PICTURE"
                          Try
                            If .Item(k).Value.GetType Is GetType(Byte()) Then
                              Dim ms As New System.IO.TagStreamMem(CType(.Item(k).Value, Byte()))
                              Dim length As Long = ms.Length
                              Dim picType As Tags.ID3.ID3v2Frames.BinaryFrames.AttachedPictureFrame.PictureTypes = CType(ms.ReadByte, Tags.ID3.ID3v2Frames.BinaryFrames.AttachedPictureFrame.PictureTypes)
                              length -= 1
                              If picType = BinaryFrames.AttachedPictureFrame.PictureTypes.Cover_Front Then
                                Dim picLength As UInteger = ms.ReadUInt(4) ' Don't use this value; it's not right.  I think the endedness is reversed.  Use just 'length' instead (that's why we are tracking it)
                                length -= 4
                                Dim mimeType As String = ms.ReadText(length, TextEncodings.UTF_16, length, True)
                                Dim desc As String = ms.ReadText(length, TextEncodings.UTF_16, length, True)
                                Dim msbmp As IO.MemoryStream = ms.ReadData(length)
                                Dim bmp As New Bitmap(msbmp)
                                ft.AlbumArt = CreateAblumArtThumbnail(bmp, 480)
                                ft.AlbumArtThumbnail = CreateAblumArtThumbnail(bmp, _thumbHeight)
                                msbmp.Close()
                              End If
                              ms.Close()
                            End If
                          Catch ex As Exception
                            UpdateAudioFileInDatabase &= "UpdateAudioFileInDatabase: " & file & " - WMA Picture Parsing Failed." & vbCrLf & ex.ToString
                          End Try
                      End Select
                    Next
                  End With

                  ' This might be overridden as an extended tag
                  If ft.Artist.Length <= 0 Then ft.Artist = StringNoNull(asf.ContentDescription.Author)

                  ' Populate the normal tags
                  ft.Title = StringNoNull(asf.ContentDescription.Title)
                  ft.Copyright = StringNoNull(asf.ContentDescription.Copyright)
                  ft.Comments = StringNoNull(asf.ContentDescription.Description)

                  getTagFailed = False
                Catch ex As Exception
                  UpdateAudioFileInDatabase &= "UpdateAudioFileInDatabase: " & file & " - WMA Tag Parsing Failed." & vbCrLf & ex.ToString
                End Try

              Case Settings.AudioFileType.WAV ' Parse WAV here! -----------------------------------------------------------------------------------------
                Try
                  ' There is no tag parsing in WAV files, so we just end here and let the the missing fields be filled in below by path information
                  ft.Format = "WAV"
                  getTagFailed = False
                Catch ex As Exception
                  UpdateAudioFileInDatabase &= "UpdateAudioFileInDatabase: " & file & " - WAV Tag Parsing Failed." & vbCrLf & ex.ToString
                End Try


              Case Else
                UpdateAudioFileInDatabase &= "UpdateAudioFileInDatabase: " & file & " - Unsupported file type." & vbCrLf
                Exit Try
            End Select

            ' Set file information
            ft.FilePath = file
            ft.FileModifyDate = fi.LastWriteTime
            ft.FileSize = fi.Length

            ' Set the last found date
            ft.LastFoundDate = Date.Now

            ' Fill in any missing tags by parsing data out of the file path
            If Settings.OverrideTagsWithTagsFromPath Then
              FillFieldsFromFilePath(ft, True)
            ElseIf Settings.FillMissingTagsFromPath Then
              FillFieldsFromFilePath(ft, False)
            End If

            ' Is this file a podcast?
            With New IsPodcast
              .FilePath = ft.FilePath
              .Artist = ft.Artist
              .Album = ft.AlbumArtist
              .Title = ft.Title
              .Album = ft.Album
              .Genre = ft.Genre
              .Comments = ft.Comments
              ft.IsPodcast = .IsPodcast
            End With

            ' Are critical fields still missing?  If so, supply a default "Unknown" value
            If ft.Artist.Length <= 0 Then
              ft.Artist = GetLanguageString("NoArtist")
            End If
            If ft.Title.Length <= 0 Then
              ft.Title = GetLanguageString("NoTitle")
            End If

            ' If artist or album artist is empty, fill in the other value for consistent searching
            If Settings.FillMissingArtistOrAlbumArtistTags Then
              If ft.Artist.Trim.Length <= 0 AndAlso ft.AlbumArtist.Trim.Length > 0 Then
                ft.Artist = ft.AlbumArtist
              ElseIf ft.Artist.Trim.Length > 0 AndAlso ft.AlbumArtist.Trim.Length <= 0 Then
                ft.AlbumArtist = ft.Artist
              End If
            End If

            ' Calculate MD5 hash
            If Settings.CalculateMD5Hashes Then
              Try
                ft.MD5Hash = AvianPlayDB.MD5FileHash(ft.FilePath)
              Catch ex As Threading.ThreadAbortException
                Throw
              Catch ex As Exception
                ' This may fail if the file is in ROM.  That's okay because it can't move if it's in ROM, which is the only reason for
                ' the MD5 hash anyway. :-)
                'System.IO.FileAttributes.Device = FILE_ATTRIBUTE_INROM
                'System.IO.FileAttributes.Offline = FILE_ATTRIBUTE_ROMSTATICREF
                'System.IO.FileAttributes.NotContentIndexed = FILE_ATTRIBUTE_ROMMODULE
                If (fi.Attributes And IO.FileAttributes.Device) = IO.FileAttributes.Device _
                 OrElse (fi.Attributes And IO.FileAttributes.Offline) = IO.FileAttributes.Offline _
                 OrElse (fi.Attributes And IO.FileAttributes.NotContentIndexed) = IO.FileAttributes.NotContentIndexed _
                Then
                  ' This is okay!  This file is in ROM.
                Else
                  UpdateAudioFileInDatabase &= "UpdateAudioFileInDatabase: " & file & " - MD5 calculation failed." & vbCrLf & ex.ToString
                End If
                ft.MD5Hash = Nothing
                ' TODO: WTF?  On the actual device, ROM files are failing and this isn't even working.
              End Try
            Else
              ft.MD5Hash = Nothing
            End If

            ' Check to see if this file is a file that may have moved to a new path; if so, update the path and set the update action to "update"
            If ua <> UpdateAction.Update AndAlso ft.MD5Hash IsNot Nothing Then
              For Each mft As AvianPlayDB.MD5FileTable In db.GetNotFoundMD5AudioFileHashes
                If AvianPlayDB.AreByteArraysEqual(mft.MD5Hash, ft.MD5Hash) Then
                  db.UpdateAudioFileDBFilePath(mft.FilePath, ft.FilePath)
                  ua = UpdateAction.Update
                  Exit For
                End If
              Next
            End If

            ' Check to see if we need to adjust the 'last updated' flag because the file couldn't be read (in other words, we're relying potentially only on path-parsed values and not tags)
            If getTagFailed OrElse readFailed OrElse Not CanReadFile(file) Then
              ft.FileModifyDate = AvianPlayDB.MinimumDateTime ' (Note: insert will insert values whether or not 'getTagFailed' but update will only update if 'getTagFailed' is false)
            End If

            ' Insert or update the fields based on what we just retrieved
            If ua = UpdateAction.Insert Then
              ' Unlike below, if tag reading failed, insert the file anyway, but it will have minimum modify date time from above, so it will get rescanned as soon as possible...
              db.InsertAudioFileDBData(ft)
            ElseIf ua = UpdateAction.Update Then
              If getTagFailed Then
                ' Don't update if tag reading failed on the file.  Even if there are no tags, tag reading should not fail (exception during tag reading).  But DO update the modify date to minvalue, so it will be rescanned as soon as possible.
                db.SetAudioFileModifyDate(ft.FilePath) = AvianPlayDB.MinimumDateTime
              Else
                ' Update the file
                db.UpdateAudioFileDBData(ft)
              End If
            End If
          Else
            ' The file has not changed, so just update the "last found date"
            db.SetAudioFileLastFoundDate(file) = Date.Now
          End If
        End If
      Catch ex As Threading.ThreadAbortException
        Throw
      Catch ex As Exception
        If UpdateAudioFileInDatabase.Length > 0 Then UpdateAudioFileInDatabase &= vbCrLf & vbCrLf
        UpdateAudioFileInDatabase &= "UpdateAudioFileInDatabase: " & file & vbCrLf & ex.ToString
      End Try
    End Function

    Friend Shared Function UpdatePlaylistFileInDatabase(ByVal file As String, ByVal em As Settings.PlaylistExtensionMap, ByVal db As AvianPlayDB) As String
      UpdatePlaylistFileInDatabase = ""
      Try
        ' Make sure the file both exists and the size is larger than 0
        If IO.File.Exists(file) AndAlso New IO.FileInfo(file).Length > 0 Then
          Dim readFailed As Boolean = Not CanReadFile(file) ' Check to see if we can read the file

          Dim ua As UpdateAction = UpdateAction.NoAction
          Dim fi As New System.IO.FileInfo(file) ' Get file information
          Dim ft As AvianPlayDB.PlaylistFileTable = Nothing
          Dim avpl As Playlists.AVPLPlaylist = Nothing

          ' Do we have any database entry for this file (found in the last scan or not)?
          Dim fDate As Date = db.GetPlaylistFileLastModifyDate(file, True)

          If fDate > Date.MinValue Then
            ' We have an entry for this file, check the update times
            If fDate <> fi.LastWriteTime Then
              ua = UpdateAction.Update    ' Modify times are different, so we update the entry
            Else
              ua = UpdateAction.NoAction  ' Modify times are identical, no action is needed, just update the last scan time
            End If
          Else
            ua = UpdateAction.Insert      ' Insert the new item into the database
          End If

          If ua <> UpdateAction.NoAction Then
            ' Reset the file table structure
            ft = New AvianPlayDB.PlaylistFileTable

            Try
              Select Case em.Type
                Case PlaylistFileType.AVPL
                  ft.Format = "AVPL"
                  avpl = Playlists.PlaylistReader.ParseAVPLFile(file)

                Case PlaylistFileType.M3U
                  ft.Format = "M3U"
                  avpl = Playlists.AVPLPlaylist.FromStandardPlaylist(Playlists.PlaylistReader.ParseM3UFile(file))

                Case PlaylistFileType.PLS
                  ft.Format = "PLS"
                  avpl = Playlists.AVPLPlaylist.FromStandardPlaylist(Playlists.PlaylistReader.ParsePLSFile(file))

                Case Else
                  UpdatePlaylistFileInDatabase &= "UpdatePlaylistFileInDatabase: " & file & " - Unsupported file type." & vbCrLf
                  Exit Try
              End Select
            Catch ex As Exception
              UpdatePlaylistFileInDatabase &= "UpdatePlaylistFileInDatabase: " & file & " - Unable to load playlist file." & vbCrLf & vbCrLf & ex.ToString & vbCrLf
            End Try

            If avpl Is Nothing Then
              UpdatePlaylistFileInDatabase &= "UpdatePlaylistFileInDatabase: " & file & " - Failed reading playlist file." & vbCrLf
              Exit Try
            End If

            ' Set file information
            ft.FilePath = file
            ft.FileModifyDate = fi.LastWriteTime
            ft.Title = ft.FileNameWithoutExtension ' Set the title to the filename (minus extension and path)

            ' Set the last found date
            ft.LastFoundDate = Date.Now

            ' Find out what this playlist contains
            ft.ContainsFiles = False
            ft.ContainsStreams = False
            For Each avple As Playlists.AVPLPlaylistEntry In avpl.Entries
              If avple.IsFile Then
                ft.ContainsFiles = True
              ElseIf avple.IsStream Then
                ft.ContainsStreams = True
              End If
            Next

            ' Calculate MD5 hash
            If Settings.CalculateMD5Hashes Then
              Try
                ft.MD5Hash = AvianPlayDB.MD5FileHash(ft.FilePath)
              Catch ex As Threading.ThreadAbortException
                Throw
              Catch ex As Exception
                ' This may fail if the file is in ROM.  That's okay because it can't move if it's in ROM, which is the only reason for
                ' the MD5 hash anyway. :-)
                'System.IO.FileAttributes.Device = FILE_ATTRIBUTE_INROM
                'System.IO.FileAttributes.Offline = FILE_ATTRIBUTE_ROMSTATICREF
                'System.IO.FileAttributes.NotContentIndexed = FILE_ATTRIBUTE_ROMMODULE
                If (fi.Attributes And IO.FileAttributes.Device) = IO.FileAttributes.Device _
                 OrElse (fi.Attributes And IO.FileAttributes.Offline) = IO.FileAttributes.Offline _
                 OrElse (fi.Attributes And IO.FileAttributes.NotContentIndexed) = IO.FileAttributes.NotContentIndexed _
                Then
                  ' This is okay!  This file is in ROM.
                Else
                  UpdatePlaylistFileInDatabase &= "UpdatePlaylistFileInDatabase: " & file & " - MD5 calculation failed." & vbCrLf & ex.ToString
                End If
                ft.MD5Hash = Nothing
                ' TODO: WTF?  On the actual device, ROM files are failing and this isn't even working.
              End Try
            Else
              ft.MD5Hash = Nothing
            End If

            ' Check to see if this file is a file that may have moved to a new path; if so, update the path and set the update action to "update"
            If ua <> UpdateAction.Update AndAlso ft.MD5Hash IsNot Nothing Then
              For Each mft As AvianPlayDB.MD5FileTable In db.GetNotFoundMD5PlaylistFileHashes
                If AvianPlayDB.AreByteArraysEqual(mft.MD5Hash, ft.MD5Hash) Then
                  db.UpdatePlaylistFileDBFilePath(mft.FilePath, ft.FilePath)
                  ua = UpdateAction.Update
                  Exit For
                End If
              Next
            End If

            ' Check to see if we need to adjust the 'last updated' flag because the file couldn't be read (in other words, we're relying potentially on incorrect values)
            If readFailed OrElse Not CanReadFile(file) Then
              ft.FileModifyDate = Date.MinValue
            End If

            ' Insert or update the fields based on what we just retrieved
            If ua = UpdateAction.Insert Then
              db.InsertPlaylistFileDBData(ft)
            ElseIf ua = UpdateAction.Update Then
              db.UpdatePlaylistFileDBData(ft)
            End If
          Else
            ' The file has not changed, so just update the "last found date"
            db.SetPlaylistFileLastFoundDate(file) = Date.Now
          End If
        End If
      Catch ex As Threading.ThreadAbortException
        Throw
      Catch ex As Exception
        If UpdatePlaylistFileInDatabase.Length > 0 Then UpdatePlaylistFileInDatabase &= vbCrLf & vbCrLf
        UpdatePlaylistFileInDatabase &= "UpdatePlaylistFileInDatabase: " & file & vbCrLf & ex.ToString
      End Try
    End Function

    Public Shared Sub FillFieldsFromFilePath(ByRef ft As AvianPlayDB.AudioFileTable, ByVal override As Boolean)
      Dim rx As New RegularExpressions.Regex(Settings.TagsFromPathRegEx, RegularExpressions.RegexOptions.IgnoreCase)
      With rx.Match(ft.FilePath)
        If .Success Then
          If .Groups("A") IsNot Nothing AndAlso .Groups("A").Captures.Count > 0 Then
            If ft.Artist.Trim.Length <= 0 OrElse override Then
              ft.Artist = .Groups("A").Captures(0).Value.Trim
            End If
          End If
          If .Groups("T") IsNot Nothing AndAlso .Groups("T").Captures.Count > 0 Then
            If ft.Title.Trim.Length <= 0 OrElse override Then
              ft.Title = .Groups("T").Captures(0).Value.Trim
            End If
          End If
          If .Groups("L") IsNot Nothing AndAlso .Groups("L").Captures.Count > 0 Then
            If ft.Album.Trim.Length <= 0 OrElse override Then
              ft.Album = .Groups("L").Captures(0).Value.Trim
            End If
          End If
          If .Groups("B") IsNot Nothing AndAlso .Groups("B").Captures.Count > 0 Then
            If ft.AlbumArtist.Trim.Length <= 0 OrElse override Then
              ft.AlbumArtist = .Groups("B").Captures(0).Value.Trim
            End If
          End If
          If .Groups("N") IsNot Nothing AndAlso .Groups("N").Captures.Count > 0 Then
            If ft.TrackNumber <= 0 OrElse override Then
              Dim num As String = .Groups("N").Captures(0).Value.Trim
              If IsNumeric(num) Then
                Try
                  ft.TrackNumber = Integer.Parse(num)
                  If ft.TrackNumber <= 0 Then
                    ft.TrackNumber = 0
                  End If
                Catch ex As Threading.ThreadAbortException
                  Throw
                Catch
                  ft.TrackNumber = 0
                End Try
              End If
            End If
          End If
          If .Groups("G") IsNot Nothing AndAlso .Groups("G").Captures.Count > 0 Then
            If ft.Genre.Trim.Length <= 0 OrElse override Then
              ft.Genre = .Groups("G").Captures(0).Value.Trim
            End If
          End If
        End If
      End With
    End Sub

    Public Shared Function ParseID3V2TrackString(ByVal track As String) As Integer
      ParseID3V2TrackString = -1
      If track.Contains("/") Then
        Try
          ParseID3V2TrackString = Integer.Parse(track.Split("/")(0))
        Catch ex As Threading.ThreadAbortException
          Throw
        Catch
        End Try
      End If

      If ParseID3V2TrackString < 0 Then
        Try
          ParseID3V2TrackString = Integer.Parse(track)
        Catch ex As Threading.ThreadAbortException
          Throw
        Catch
          ParseID3V2TrackString = 0
        End Try
      End If
    End Function

    Public Shared Function FindYearInDateString(ByVal str As String) As Integer
      If str.Trim.Length > 0 Then
        Try
          ' First, it might just be a year, so the parse might work
          Return Integer.Parse(str)
        Catch
        End Try
        Try
          ' Otherwise, try parsing it as a full date string
          Return New Date(str).Year
        Catch
        End Try
      End If

      ' Can't find a date :-(
      Return 0
    End Function

    Public Shared Function CreateAblumArtThumbnail(ByVal srcBmp As Bitmap, ByVal height As Integer) As Bitmap
      ' Check to see if we need to resize the picture
      If srcBmp.Width > height OrElse srcBmp.Height > height Then
        Dim srcbmp2 As Bitmap

        ' Create the new scaled bitmap
        If srcBmp.Width > srcBmp.Height Then
          srcbmp2 = New Bitmap(height, (height / srcBmp.Width) * srcBmp.Height)
        Else
          srcbmp2 = New Bitmap((height / srcBmp.Height) * srcBmp.Width, height)
        End If

        ' Resize the bitmap
        ScaleBitmap(srcbmp2, srcBmp)

        Return srcbmp2
      Else
        Return New Bitmap(srcBmp)
      End If
    End Function

    Private Sub ThreadCheckTimer_Tick() Handles _threadCheckTimer.Tick
      Static lastAdvNotifString As String = ""
      If Me._rescanThread Is Nothing Then
        Me._threadCheckTimer.Enabled = False
        Me._killRescanThread = False
        Me.RaiseMonitorUpdateStatusChanged(False)
      ElseIf Settings.ShowDetailedLibraryMonitorMessages Then
        If lastAdvNotifString <> Me._advNotifString Then
          Me.RaiseMonitorUpdateStatusChanged(True)
          lastAdvNotifString = Me._advNotifString
        End If
      End If
    End Sub

    Private Sub RaiseMonitorUpdateStatusChanged(ByVal isUpdating As Boolean)
      Me._lastMonitorStatus = isUpdating
      RaiseEvent MonitorUpdateStatusChanged(Me._lastMonitorStatus, Me._lastRealtimeMonitorStauts)
    End Sub

    Private Sub RealtimeMonitor_WriteErrorOnMainThread(ByVal err As String) Handles _realtimeMonitor.WriteErrorOnMainThread
      RaiseEvent WriteErrorOnMainThread(err)
    End Sub

    Private Sub RealtimeMonitor_DoFullScan() Handles _realtimeMonitor.DoFullScan
      If Me.IsMonitorFullScanActive Then
        Me._scanAgain = True
      Else
        Me._scanAgain = False
        RaiseEvent StartFullScanAfterDelayOnMainThread()
      End If
    End Sub

    Private Sub RealtimeMonitor_WriteErrorOnMainThread(ByVal oldFileName As String, ByVal newFileName As String) Handles _realtimeMonitor.UpdateManagedPlaylists
      RaiseEvent RealtimeUpdateManagedPlaylists(oldFileName, newFileName)
    End Sub

    Private Sub RealtimeMonitor_MonitorUpdateStatusChanged(ByVal isUpdating As Boolean, ByVal path As String) Handles _realtimeMonitor.MonitorUpdateStatusChanged
      If isUpdating AndAlso Settings.ShowDetailedLibraryMonitorMessages Then
        Me._advNotifString = GetLanguageString("AdvScanning").Replace("%", path)
      End If

      Me._lastRealtimeMonitorStauts = isUpdating
      RaiseEvent MonitorUpdateStatusChanged(Me._lastMonitorStatus, Me._lastRealtimeMonitorStauts)
    End Sub
  End Class
End Namespace